feat: OpenRouter provider adapter - #204
Conversation
Adds `openrouter` as a first-class provider type. OpenRouter fronts many model providers behind one OpenAI-compatible endpoint, one API key, and one prepaid balance, so the adapter is a thin delegation layer over the OpenAI adapter's translation helpers. What it adds is exactly the OpenRouter-specific surface: - default base_url https://openrouter.ai/api/v1 (still overridable); api_key falls back to OPENROUTER_API_KEY and fails loud when unresolvable, never falling through to the SDK's OPENAI_API_KEY - HTTP-Referer / X-Title attribution headers - error-mapping deltas, live-verified where possible: 400 "not a valid model ID" -> LLMNotFoundError (OpenRouter does not 404 for unknown models), 402 credits-exhausted -> LLMAuthError with a top-up hint, 403 moderation-flag -> LLMRefusalError, and finish_reason == "error" (mid-generation provider failure) -> LLMResponseError instead of warn-and-normalise - 15 pricing records in config/models.json for the current Claude/GPT/Gemini models under their OpenRouter vendor/model slugs, rates pulled from the live OpenRouter catalogue (2026-08-01) - openai.py's shared warnings/errors now take an adapter= name so reused code attributes messages to OpenRouterAdapter - contract-harness row (reusing the OpenAI scenario scripts) plus 16 OpenRouter-specific tests; README shipped-adapters entry Live-validated against OpenRouter: validate(), a text round, a full tool round-trip, and the unknown-model mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds utilities/llm/providers/OPENROUTER.md — prerequisites, full config.json example, key resolution order, model-slug conventions, cost accounting, an error/troubleshooting table, and current limitations — and links it from the README's shipped-adapters table. Placed next to the adapter code (the repo convention, cf. the parser PARSER_PIPELINE.md files) because docs/ is gitignored — which is also why the README's HOW_TO_ADD_AN_ADAPTER.md link resolves to nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Added a detailed usage guide in 33cc127: |
Lands the OpenRouter adapter (rebased from #204) plus review follow-ups (setup-wizard wiring, OpenRouter-specific probe endpoint, 403 heuristic). Co-authored-by: NahumKorda <nahum.korda@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks so much for this, @NahumKorda — the OpenRouter adapter has landed on master via #226. Same story: master is squash-only, so this needed a rebase onto current master (the #206/#208 empty-content + Responses-path hardening had landed) plus follow-ups (setup-wizard wiring with an OpenRouter-specific probe endpoint, a sharper 403 moderation-vs-auth heuristic). Your commits were carried with authorship preserved, and the squash-merge carries a |
Adds
openrouteras a first-class provider type, per the roadmap's "More provider adapters" item; related to #9 and the pricing-accounting invariants of #65. Complements #203 (Bedrock) — the two are independent; whichever merges second has a trivial conflict inproviders/__init__.py,model_registry.py,models.json, and the README table.Why a dedicated adapter (vs.
type: "openai"+base_url)The README already suggests OpenRouter can be reached through the OpenAI adapter with a
base_urloverride, and a live gap-analysis confirmed that mostly works — text rounds and full tool round-trips pass. A dedicated type closes what doesn't:"... is not a valid model ID", not 404 (live-verified), so the generic mapping buried it inLLMResponseError; it now maps toLLMNotFoundErrorand the registry's init-timevalidate()reports it as the typo it is.vendor/modelslugs (anthropic/claude-sonnet-4.6, ...) had noconfig/models.jsonrecords. This PR adds 15, covering the current Claude/GPT/Gemini models, with rates pulled from the live OpenRouter catalogue (GET /api/v1/models, 2026-08-01).api_keyfalls back toOPENROUTER_API_KEYand fails loud when unresolvable — never falling through to the SDK'sOPENAI_API_KEYdefault, which would quietly send an OpenAI key to a third party.LLMAuthErrorwith a top-up hint; 403 is OpenRouter's moderation signal →LLMRefusalError(a moderated scan prompt must not read as an auth failure — or as a clean pass); a provider failing mid-generation comes back as HTTP 200 withfinish_reason == "error"→ raised asLLMResponseErrorinstead of warn-and-normalise.HTTP-Referer/X-Title), per OpenRouter's app-attribution docs.The wire format is OpenAI's, so the adapter reuses the OpenAI adapter's translation helpers (
openai.py's shared warnings take anadapter=name so reused code attributes messages correctly)._token_paramalready strips vendor prefixes, soopenai/o3-style slugs getmax_completion_tokensfor free.Testing
validate(), a text round, a fullecho-tool round-trip onanthropic/claude-sonnet-4.6, and the unknown-model →LLMNotFoundErrormapping.Notes for reviewers
openant setup llm) is intentionally out of scope, matching feat: AWS Bedrock provider adapter #203; the README documents hand-authored config.🤖 Generated with Claude Code